﻿/*
 * Implementacja notyfikacji na podstawie "Custom soft key Notifications" by Christopher Fairbairn http://www.christec.co.nz/blog/archives/150
 */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.WindowsCE.Forms;

namespace PocketGPW
{
    [StructLayout(LayoutKind.Sequential)]
    internal struct SHNOTIFICATIONDATA
    {
        // for verification and versioning
        public int cbStruct;
        // identifier for this particular notification
        public int dwID;
        // priority
        public SHNP npPriority;
        /// <summary>
        /// Duration to display the bubble (in seconds).
        /// </summary>
        public int csDuration;
        // the icon for the notification
        public IntPtr hicon;
        /// <summary>
        /// Flags that affect the behaviour of the Notification bubble
        /// </summary>
        public SHNF grfFlags;
        // unique identifier for the notification class
        public Guid clsid;
        // window to receive command choices, dismiss, etc.
        public IntPtr hwndSink;
        // HTML content for the bubble
        [MarshalAs(UnmanagedType.LPTStr)]
        public string pszHTML;
        // Optional title for bubble
        [MarshalAs(UnmanagedType.LPTStr)]
        public string pszTitle;
        /// <summary>
        /// User defined parameter
        /// </summary>
        public int lParam;

        // Structure fields below this point were added in WM5.0
        public SOFTKEYNOTIFY leftSoftKey;
        public SOFTKEYNOTIFY rightSoftKey;
        [MarshalAs(UnmanagedType.LPTStr)]
        public string pszTodaySK;
        [MarshalAs(UnmanagedType.LPTStr)]
        public string pszTodayExec;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct SOFTKEYCMD
    {
        public UInt32 wpCmd;
        public UInt32 grfFlags;

    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct SOFTKEYNOTIFY
    {
        [MarshalAs(UnmanagedType.LPTStr)]
        public String pszTitle;
        public SOFTKEYCMD skc;
    }

    internal enum SHNP : int
    {
        /// <summary>
        /// Bubble shown for duration, then goes away
        /// </summary>
        INFORM = 0x1B1,
        /// <summary>
        /// No bubble, icon shown for duration then goes away
        /// </summary>
        ICONIC = 0,
    }

    // notification update mask
    internal enum SHNUM : int
    {
        PRIORITY = 0x0001,
        DURATION = 0x0002,
        ICON = 0x0004,
        HTML = 0x0008,
        TITLE = 0x0010,
        SOFTKEYS = 0x0020,
        TODAYKEY = 0x0040,
        TODAYEXEC = 0x0080,
        SOFTKEYCMDS = 0x0100,
        FLAGS = 0x0200
    }

    /// <summary>
    /// Flags that affect the display behaviour of the Notification
    /// </summary>
    internal enum SHNF : int
    {
        /// <summary>
        /// For SHNP_INFORM priority and above, don't display the notification bubble when it's initially added;
        /// the icon will display for the duration then it will go straight into the tray.
        /// The user can view the icon / see the bubble by opening the tray.
        /// </summary>
        STRAIGHTTOTRAY = 0x00000001,
        /// <summary>
        /// Critical information - highlights the border and title of the bubble.
        /// </summary>
        CRITICAL = 0x00000002,
        /// <summary>
        /// Force the message (bubble) to display even if settings says not to.
        /// </summary>
        FORCEMESSAGE = 0x00000008,
        /// <summary>
        /// Force the display to turn on for notification. Added for Windows Mobile 2003.
        /// </summary>
        DISPLAYON = 0x00000010,
        /// <summary>
        /// Force the notification to be silent and not vibrate, regardless of Settings. Added for Windows Mobile 2003.
        /// </summary>
        SILENT = 0x00000020,

        // Draw the current time with the title
        TITLETIME = 0x00000080,

        // A notification with "stack" support
        SPINNERS = 0x00000100,

        // RE-play physical alerts on an update
        SHNF_ALERTONUPDATE = 0x00000200
    }

    internal struct NMSHN
    {
        public IntPtr hwndFrom;
        public int idFrom;
        public SHNN code;
        public int lParam;
        public int dwReturn;
        public int union1;
        public int union2;
    }

    internal enum SHNN : int
    {
        LINKSEL = -1000,
        DISMISS = -1001,
        SHOW = -1002,
        NAVPREV = -1003,  // Toast stack left spinner clicked / DPAD LEFT
        NAVNEXT = -1004,  // Toast stack right spinner clicked / DPAD RIGHT
        /*    
        #define SHNN_ACTIVATE           (SHNN_FIRST-5)  // Toast DPAD Action
        #define SHNN_ICONCLICKED        (SHNN_FIRST-6)  // nmshn.pt contains the point where the user clicked
        #define SHNN_HOTKEY             (SHNN_FIRST-7)  
          // A hotkey has been pressed - modifiers are in the loword of the nmshn.lParam, 
          // the virtual key code is in the hiword.
          // If the sink window returns 0 in response to this notification, then
          // the notification toast will be hidden and VK_TTALK key default behavior
          // will be performed.
        */
    }

    /// <summary>
    /// Implements Windows CE functionality for displaying and responding to user notifications.
    /// <para><b>New in v1.3</b></para>
    /// </summary>
    /// <remarks>This class provides a managed implementation of the Windows CE notification functions.
    /// This class is supported only on the Pocket PC.
    /// <para>You can create notifications and then display them as needed using the <see cref="Visible"/> property.
    /// The <see cref="InitialDuration"/> property sets the time the message balloon initially displays.
    /// If you set <see cref="InitialDuration"/> to zero and Visible to true, the message balloon does not appear but its icon is available in the title bar for reactivation when clicked.
    /// The <see cref="BalloonChanged"/> event occurs whenever the balloon is shown or hidden, either programmatically using the <see cref="Visible"/> property or through user interaction.
    /// In addition to plain text, you can create a user notification with HTML content in the message balloon.
    /// The HTML is rendered by the Pocket PC HTML control, and you can respond to values in an HTML form by parsing a response string provided by the <see cref="ResponseSubmittedEventArgs"/> class, through the <see cref="ResponseSubmittedEventArgs.Response"/> property.</para>
    /// <b>Cmd:2 Identifier</b>
    /// <para>The identifier "cmd:2" has a special purpose in Windows CE and is used to dismiss notifications.
    /// If cmd:2 is the name of an HTML button or other element in a message balloon, the <see cref="ResponseSubmitted"/> event is not raised.
    /// The notification is dismissed, but its icon is placed on the title bar to be responded to at a later time.</para></remarks>
    public partial class Notification : Component
    {
        private static NotificationMessageWindow msgwnd;
        private static Dictionary<int, Notification> notifications;
        private static int id = 1;
        private static Guid clsid = Guid.NewGuid();

        private SHNOTIFICATIONDATA m_data;
        private Icon mIcon = null;
        private bool mVisible = false;
        private bool mCreated = false;

        private NotificationSoftKey mLeftSoftKey = null;
        private NotificationSoftKey mRightSoftKey = null;

        static Notification()
        {
            notifications = new Dictionary<int, Notification>();

            msgwnd = new NotificationMessageWindow(notifications);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Notification"/> class.
        /// </summary>
        /// <remarks>This class is not supported on the Smartphone or other Windows CE devices that are not Pocket PCs.
        /// You can create multiple notifications, such as an array of notifications, and display them as needed with the Visible property.</remarks>
        public Notification()
        {
            m_data = new SHNOTIFICATIONDATA();

            if (PlatformSupportsCustomSoftKeyButtons)
                m_data.cbStruct = Marshal.SizeOf(m_data);
            else
                m_data.cbStruct = Marshal.SizeOf(m_data) - 32; // "hide" the 20 bytes that were added to this struct in WM5.0    

            m_data.clsid = clsid;
            m_data.dwID = id;
            m_data.hwndSink = msgwnd.Hwnd;
            m_data.csDuration = 10;

            notifications.Add(id, this);

            id++;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
            // remove from notification collection
            Visible = false;
            notifications.Remove(m_data.dwID);

            base.Dispose(disposing);
        }

        // Fill out the SOFTKEYNOTIFY structure to create the notification soft key type specified
        // within 'data'.
        private void ConfigureSoftKey(ref SOFTKEYNOTIFY softKey, NotificationSoftKey data, uint index)
        {
            if (data != null)
            {
                softKey.pszTitle = String.IsNullOrEmpty(data.Title) ? " " : data.Title;
                softKey.skc.grfFlags = (uint)data.Type;
            }
            else
            {
                // NULL means an "empty" soft key, the best way
                // to simulate this is to make a soft key which has
                // a blank title (does not render any text) and
                // is disabled (so it doesn't respond to stylus
                // taps).
                softKey.pszTitle = " ";
                softKey.skc.grfFlags = (uint)NotificationSoftKeyType.Disabled;
            }

            // Each softkey must have a unique id for use with WM_COMMAND
            // so fudge the notification id and softkey index into a unique
            // id which we can easily reverse back into it's components.
            softKey.skc.wpCmd = (uint)(m_data.dwID << 8) + index;
        }

        private void RebuildSoftKeys()
        {
            if (PlatformSupportsCustomSoftKeyButtons)
            {
                // Configure the soft key part of the structure
                ConfigureSoftKey(ref m_data.leftSoftKey, mLeftSoftKey, 0);
                ConfigureSoftKey(ref m_data.rightSoftKey, mRightSoftKey, 1);
            }
        }

        // Custom SoftKey menus are supported on Pocket PC devices running
        // Windows Mobile 5.0 or higher.
        public bool PlatformSupportsCustomSoftKeyButtons
        {
            get
            {
                bool isPocketPC = (GetSystemParameter(SPI_GETPLATFORMTYPE) == "PocketPC");

                return isPocketPC && Environment.OSVersion.Version.Major >= 5;
            }
        }

        /// <summary>
        /// Gets or sets a string specifying the title text for the message balloon.
        /// </summary>
        /// <value>A string that specifies the caption text.
        /// The default value is an empty string ("").</value>
        /// <remarks>The background color of the caption is dependent on the current theme of the user.
        /// Use the <see cref="Text"/> property to modify the text in the body of the message balloon.</remarks>
        public string Caption
        {
            get
            {
                return m_data.pszTitle;
            }
            set
            {
                m_data.pszTitle = value;

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.TITLE, ref m_data);
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the notification is of urgent importance.
        /// </summary>
        /// <value>true if the notification is critical; otherwise, false. The default is false.</value>
        /// <remarks>Critical notifications have a red background caption color, or other color, depending on the current Pocket PC theme.</remarks>
        public bool Critical
        {
            get
            {
                return (m_data.grfFlags & SHNF.CRITICAL) != 0;
            }
            set
            {
                if (value)
                {
                    m_data.grfFlags |= SHNF.CRITICAL;
                }
                else
                {
                    m_data.grfFlags ^= (m_data.grfFlags & SHNF.CRITICAL);
                }

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.FLAGS, ref m_data);
                }
            }
        }

        /// <summary>
        /// Gets or sets the current native icon handle for the message balloon on the title bar.
        /// </summary>
        public Icon Icon
        {
            get
            {
                return mIcon;
            }
            set
            {
                mIcon = value;
                m_data.hicon = mIcon.Handle;

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.ICON, ref m_data);
                }
            }
        }

        /// <summary>
        /// Gets or sets the number of seconds the message balloon remains visible after initially shown.
        /// </summary>
        /// <remarks>The amount of time the balloon is visible, in seconds. The default is 10.</remarks>
        public int InitialDuration
        {
            get
            {
                return m_data.csDuration;
            }
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException();
                }

                m_data.csDuration = value;

                if (m_data.csDuration == 0)
                {
                    m_data.npPriority = SHNP.ICONIC;
                }
                else
                {
                    m_data.npPriority = SHNP.INFORM;
                }

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.DURATION | SHNUM.PRIORITY, ref m_data);
                }
            }
        }

        /// <summary>
        /// Gets or sets the text for the message balloon.
        /// </summary>
        public string Text
        {
            get
            {
                return m_data.pszHTML;
            }
            set
            {
                m_data.pszHTML = value;

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.HTML, ref m_data);
                }
            }
        }

        public NotificationSoftKey LeftSoftKey
        {
            get
            {
                return mLeftSoftKey;
            }
            set
            {
                mLeftSoftKey = value;
                RebuildSoftKeys();

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.SOFTKEYS, ref m_data);
                }
            }
        }

        public NotificationSoftKey RightSoftKey
        {
            get
            {
                return mRightSoftKey;
            }
            set
            {
                mRightSoftKey = value;
                RebuildSoftKeys();

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.SOFTKEYS, ref m_data);
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the current time is displayed with the notification title. 
        /// </summary>
        public bool TitleTime
        {
            get { return (m_data.grfFlags & SHNF.TITLETIME) != 0; }
            set
            {
                if (value)
                {
                    m_data.grfFlags |= SHNF.TITLETIME;
                }
                else
                {
                    m_data.grfFlags ^= (SHNF.TITLETIME & m_data.grfFlags);
                }

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.FLAGS, ref m_data);
                }
            }
        }

        /// <summary>
        /// ???
        /// </summary>
        public bool Spinners
        {
            get { return (m_data.grfFlags & SHNF.SPINNERS) != 0; }
            set
            {
                if (value)
                {
                    m_data.grfFlags |= SHNF.SPINNERS;
                }
                else
                {
                    m_data.grfFlags ^= (SHNF.SPINNERS & m_data.grfFlags);
                }

                if (mCreated)
                {
                    SHNotificationUpdate(SHNUM.FLAGS, ref m_data);
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the message balloon is visible.
        /// </summary>
        public bool Visible
        {
            get
            {
                return mVisible;
            }
            set
            {
                if (mVisible != value)
                {
                    mVisible = value;

                    if (mVisible)
                    {
                        int hresult = SHNotificationAdd(ref m_data);
                        mCreated = true;
                    }
                    else
                    {
                        int hresult = SHNotificationRemove(ref clsid, m_data.dwID);
                        mCreated = false;
                    }
                }
            }
        }

        /// <summary>
        /// Occurs when a message balloon is displayed or hidden.
        /// </summary>
        public event BalloonChangedEventHandler BalloonChanged;

        internal void OnBalloonChanged(BalloonChangedEventArgs e)
        {
            //update visible state
            mVisible = e.Visible;

            if (this.BalloonChanged != null)
            {
                this.BalloonChanged(this, e);
            }
        }

        /// <summary>
        /// Occurs when the user clicks a button or link in the message balloon.
        /// </summary>
        public event ResponseSubmittedEventHandler ResponseSubmitted;

        internal void OnResponseSubmitted(ResponseSubmittedEventArgs e)
        {
            if (this.ResponseSubmitted != null)
            {
                this.ResponseSubmitted(this, e);
            }
        }

        /// <summary>
        /// Occurs when the user clicks a button or link in the message balloon.
        /// </summary>
        public event NotificationSpinnerClickEventHandler SpinnerClick;

        internal void OnSpinnerClick(NotificationSpinnerClickEventArgs e)
        {
            if (this.SpinnerClick != null)
            {
                this.SpinnerClick(this, e);
            }
        }

        /// <summary>
        /// Occurs when the user clicks the left soft key if it is of type SoftKeyType.StayOpen.
        /// </summary>
        public event EventHandler LeftSoftKeyClick;

        internal void OnLeftSoftKeyClick(EventArgs e)
        {
            if (this.LeftSoftKeyClick != null)
            {
                this.LeftSoftKeyClick(this, e);
            }
        }

        /// <summary>
        /// Occurs when the user clicks the right soft key if it is of type SoftKeyType.StayOpen.
        /// </summary>
        public event EventHandler RightSoftKeyClick;

        internal void OnRightSoftKeyClick(EventArgs e)
        {
            if (this.RightSoftKeyClick != null)
            {
                this.RightSoftKeyClick(this, e);
            }
        }

        #region API Declares

        //Add
        [DllImport("aygshell.dll", EntryPoint = "#155", SetLastError = true)]
        private static extern int SHNotificationAdd(ref SHNOTIFICATIONDATA shinfo);

        //Remove
        [DllImport("aygshell.dll", EntryPoint = "#157", SetLastError = true)]
        private static extern int SHNotificationRemove(ref Guid clsid, int dwID);

        //Update
        [DllImport("aygshell.dll", EntryPoint = "#156", SetLastError = true)]
        private static extern int SHNotificationUpdate(SHNUM grnumUpdateMask, ref SHNOTIFICATIONDATA shinfo);

        //Get Data
        [DllImport("aygshell.dll", EntryPoint = "#173", SetLastError = true)]
        private static extern int SHNotificationGetData(ref Guid clsid, int dwID, ref SHNOTIFICATIONDATA shinfo);

        // GetSystemParameter
        private static readonly uint SPI_GETPLATFORMTYPE = 257;

        [DllImport("coredll.dll", SetLastError = true)]
        private static extern int SystemParametersInfo(uint uiAction, uint uiParam, StringBuilder pvParam, uint fWinini);

        private static string GetSystemParameter(uint uiParam)
        {
            StringBuilder sb = new StringBuilder(128);
            if (SystemParametersInfo(uiParam, (uint)sb.Capacity, sb, 0) == 0)
                throw new ApplicationException("Failed to get system parameter");
            return sb.ToString();
        }

        #endregion
    }
}
